import { test, expect, beforeEach } from 'vitest'

import { type MoveTimeTo, setupTimers, createMoveTimeTo } from '../../__testUtils__/timers'
import { createAnimator } from '../../__testUtils__/nodes'

setupTimers()

let moveTimeTo: MoveTimeTo

beforeEach(() => {
  moveTimeTo = createMoveTimeTo()
})

test('Should create parent and transition children', async () => {
  const parent = createAnimator(undefined, { manager: 'sequence' })
  const child1 = createAnimator(parent)
  const child2 = createAnimator(parent)
  const child3 = createAnimator(parent)
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  expect(parent.node.state).toBe('exited')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.399)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.401)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.799)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.801)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(1.199)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(1.201)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(1.599)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(1.601)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')

  moveTimeTo(2)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
})

test('Should create combined parent and transition children', async () => {
  const parent = createAnimator(undefined, { combine: true, manager: 'sequence' })
  const child1 = createAnimator(parent)
  const child2 = createAnimator(parent)
  const child3 = createAnimator(parent)
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  expect(parent.node.state).toBe('exited')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.399)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.401)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.799)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.801)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(1.199)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(1.201)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')

  moveTimeTo(2)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
})

test('Should create combined parent sequenceReverse and transition children', async () => {
  const parent = createAnimator(undefined, { combine: true, manager: 'sequenceReverse' })
  const child1 = createAnimator(parent)
  const child2 = createAnimator(parent)
  const child3 = createAnimator(parent)
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  expect(parent.node.state).toBe('exited')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(0.399)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('entering')
  moveTimeTo(0.401)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('entered')
  moveTimeTo(0.799)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('entered')
  moveTimeTo(0.801)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  moveTimeTo(1.199)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  moveTimeTo(1.201)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')

  moveTimeTo(2)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
})

test('Should create combined parent and transition children with custom durations', async () => {
  const parent = createAnimator(undefined, { combine: true, manager: 'sequence' })
  const child1 = createAnimator(parent, { duration: { enter: 0.2 } })
  const child2 = createAnimator(parent, { duration: { offset: 0.1 } })
  const child3 = createAnimator(parent, { duration: { enter: 0.1, delay: 0.2 } })
  const child4 = createAnimator(parent, { duration: { enter: 0.05 } })
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => child4.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  expect(parent.node.state).toBe('exited')
  expect(child1.node.state).toBe('exited')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering') // Enters with parent
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.199)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.201)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('exited') // offset = 0.1
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.299)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.301)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering') // 0.2 + (offset=0.1) = 0.3
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.699)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.701)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited') // delay = 0.2
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.799)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  moveTimeTo(0.801)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('entering') // 0.2 + (offset=0.1) + 0.4 + 0.1 = 0.8
  moveTimeTo(0.849)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('entering')
  moveTimeTo(0.851)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('entered')
  moveTimeTo(0.898)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('entered')
  moveTimeTo(0.901)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering') // 0.2 + (offset=0.1) + 0.4 + 0.2 = 0.9
  expect(child4.node.state).toBe('entered')
  moveTimeTo(0.998)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  expect(child4.node.state).toBe('entered')
  moveTimeTo(1.001)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entered')

  moveTimeTo(2)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entered')
})

test('Should create combined parent and transition dynamically updated children', async () => {
  const parent = createAnimator(undefined, { combine: true, manager: 'sequence' })
  const child1 = createAnimator(parent)
  const child2 = createAnimator(parent)
  const child3 = createAnimator(parent)
  const child4 = createAnimator(parent)
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => child4.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')

  moveTimeTo(0.6)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  parent.system.unregister(child1.node) // Remove node already entered.
  parent.system.unregister(child4.node) // Remove node not entering yet.
  const child5 = createAnimator(parent)
  const child6 = createAnimator(parent)
  queueMicrotask(() => child5.node.send('setup'))
  queueMicrotask(() => child6.node.send('setup'))

  moveTimeTo(0.601)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(0.601)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(0.799)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(0.801)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  expect(child5.node.state).toBe('exited')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(1.199)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  expect(child5.node.state).toBe('exited')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(1.201)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entering')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(1.599)
  expect(parent.node.state).toBe('entering')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entering')
  expect(child6.node.state).toBe('exited')
  moveTimeTo(1.601)
  expect(parent.node.state).toBe('entered') // Parent initial duration was 4*0.4 = 1.6
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')
  expect(child6.node.state).toBe('entering')
  moveTimeTo(1.999)
  expect(parent.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')
  expect(child6.node.state).toBe('entering')
  moveTimeTo(2.001)
  expect(parent.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')
  expect(child6.node.state).toBe('entered')

  moveTimeTo(3)
  expect(parent.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')
  expect(child6.node.state).toBe('entered')
})

test('Should create combined parent and transition dynamically updated children with refresh event', () => {
  const parent = createAnimator(undefined, { combine: true, manager: 'sequence' })
  const child1 = createAnimator(parent, { duration: { enter: 1 } })
  const child2 = createAnimator(parent, { duration: { enter: 1 } })
  const child3 = createAnimator(parent, { duration: { enter: 1 } })
  const child4 = createAnimator(parent, { duration: { enter: 1 }, condition: () => false })
  const child5 = createAnimator(parent, { duration: { enter: 1 } })
  queueMicrotask(() => child1.node.send('setup'))
  queueMicrotask(() => child2.node.send('setup'))
  queueMicrotask(() => child3.node.send('setup'))
  queueMicrotask(() => child4.node.send('setup'))
  queueMicrotask(() => child5.node.send('setup'))
  queueMicrotask(() => parent.node.send('setup'))

  moveTimeTo(0.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entering')
  expect(child2.node.state).toBe('exited')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')

  moveTimeTo(1.5)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')

  child4.node.control.setSettings({ condition: () => true })
  queueMicrotask(() => parent.node.send('refresh'))

  moveTimeTo(1.501)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  moveTimeTo(1.999)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entering')
  expect(child3.node.state).toBe('exited')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  moveTimeTo(2.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  moveTimeTo(2.999)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entering')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('exited')
  moveTimeTo(3.001)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('exited') // Node was not added in the initial calculation.
  expect(child5.node.state).toBe('entering') // Node was first in the initial calculation.
  moveTimeTo(3.999)
  expect(parent.node.state).toBe('entering')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('exited')
  expect(child5.node.state).toBe('entering')
  moveTimeTo(4.001)
  expect(parent.node.state).toBe('entered') // Parent node initially calculated 4 children to enter.
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entering') // Node is now entering.
  expect(child5.node.state).toBe('entered')
  moveTimeTo(4.999)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entering')
  expect(child5.node.state).toBe('entered')
  moveTimeTo(5.001)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')

  moveTimeTo(7)
  expect(parent.node.state).toBe('entered')
  expect(child1.node.state).toBe('entered')
  expect(child2.node.state).toBe('entered')
  expect(child3.node.state).toBe('entered')
  expect(child4.node.state).toBe('entered')
  expect(child5.node.state).toBe('entered')
})

test.todo('Should create combined parent and transition children with sequenceLimit')
